const path = require('path');
const fs = require('fs');
const babylon = require('babylon');
const traverse = require('@babel/traverse').default; // 这个是 es6模块,如果不default 默认拿到的是一个对象
const t = require('@babel/types');
const generator = require('@babel/generator').default;
const ejs = require('ejs');
const { SyncHook } = require('tapable');

// babylon 主要就是把源码转换为 AST
// @babel/traverse 遍历节点
// @babel/types 节点替换
// @babel/generator 生成
class Compiler {
    constructor(config) {
        this.config = config;
        // 入口文件ID
        this.entryId; // './src/index.js'
        this.modules = {}; // 需要保存所有的模块依赖
        this.entry = config.entry; // 入口路径
        this.root = process.cwd(); // 工作路径
        this.assets = {}; // 资源 
        // 钩子
        this.hooks = {
            entryOption: new SyncHook(),
            compile: new SyncHook(),
            afterCompile: new SyncHook(),
            afterPulgins: new SyncHook(),
            run: new SyncHook(),
            emit: new SyncHook(),
            done: new SyncHook()
        };
        // 注册插件，并执行 apply方法
        const plugins = this.config.plugins;
        if (Array.isArray(plugins)) {
            plugins.forEach((plugin) => {
                plugin.apply(this);
            });
        }
        this.hooks.afterPulgins.call();
    }


    /**
     * 获取模块源码
     * @param {} filePath 模块路径 
     */
    getSource(filePath) {
        return fs.readFileSync(filePath, 'utf8');
    }

    /**
     * 解析源码
     * @param {*} source 源码 
     * @param {*} parentPaht 父路径
     */
    parse(source, parentPaht) {
        // 模块依赖
        let dependencies = [];
        // 解析为AST
        let ast = babylon.parse(source);
        // https://astexplorer.net
        traverse(ast, {
            CallExpression(p) {
                let node = p.node; // 对应的节点
                if (node.callee.name === 'require') { // 说明有依赖
                    node.callee.name = '__webpack_require__';
                    let moduleName = node.arguments[0].value // 取到的就是模块的引用名称 'a.js'
                    moduleName = moduleName + (path.extname(moduleName) ? '' : '.js');
                    moduleName = './' + path.join(parentPaht, moduleName); './src/a.js';
                    dependencies.push(moduleName);
                    // require('a.js') 改为 __webpack_require__('./src/a.js');
                    node.arguments = [t.stringLiteral(moduleName)];
                }
            }
        });
        let sourceCode = generator(ast).code;
        return { sourceCode, dependencies };
    }

    /**
     * 创建模块依赖
     * @param {*} pathModule 模块的绝对路径 c://xxx/xxx.js 
     * @param {*} isEntry 是否入口模块
     */
    buildModule(modulePath, isEntry) {
        // 拿到源码
        let source = this.getSource(modulePath);
        // 拿到规则
        const rules = this.config.module.rules;
        rules.forEach(item => {
            if (item.test.test(modulePath)) {
                const use = item.use;
                let len = use.length - 1;
                while (len >= 0) {
                    let loader = require(use[len--]);
                    source = loader(source);
                }
            }
        });


        // 得到模块名称（'./src/a.js'） = modulePath - this.root
        const moduleName = './' + path.relative(this.root, modulePath);
        if (isEntry) {
            this.entryId = moduleName;
        }
        // 解析需要把source源码进行改造，返回一个依赖列表
        // path.dirname(moduleName) = './src'
        let { sourceCode, dependencies } = this.parse(source, path.dirname(moduleName));
        // 把相对路径和模块中的内容对应起来存放在 this.modules
        this.modules[moduleName] = sourceCode;
        // 递归依赖模块
        dependencies.forEach(dep => {
            this.buildModule(path.resolve(this.root, dep), false);
        });
    }

    emitFile() {
        // 输出路径
        const outputFilename = path.resolve(this.config.output.path, this.config.output.filename);
        // 获取模板
        const templateContent = this.getSource(path.resolve(__dirname, './template.ejs'));
        // ejs 渲染
        const code = ejs.render(templateContent, { entryId: this.entryId, modules: this.modules });
        this.assets[outputFilename] = code;
        fs.writeFileSync(outputFilename, this.assets[outputFilename]);
    }

    run() {
        this.hooks.run.call();
        this.hooks.compile.call();
        // 执行，并创建模块的依赖关键
        this.buildModule(path.resolve(this.root, this.entry), true);
        this.hooks.afterCompile.call();
        // 发射一个文件，打包后的文件
        this.emitFile();
        this.hooks.emit.call();
        this.hooks.done.call();
    }
}

module.exports = Compiler;